Данные сервиса Яндекс.Недвижимость — архив объявлений о продаже квартир в Санкт-Петербурге и соседних населённых пунктов за несколько лет. Нужно научиться определять рыночную стоимость объектов недвижимости. Ваша задача — установить параметры. Это позволит построить автоматизированную систему: она отследит аномалии и мошенническую деятельность.
По каждой квартире на продажу доступны два вида данных. Первые вписаны пользователем, вторые — получены автоматически на основе картографических данных. Например, расстояние до центра, аэропорта, ближайшего парка и водоёма.
import os
import sys
from random import randint
import urllib
import numpy as np
import pandas as pd
from pathlib import Path
import plotly.express as px
import matplotlib.pyplot as plt
path = 'data/real_estate_data.csv'
url = 'https://code.s3.yandex.net/datasets/real_estate_data.csv'
Path('data').mkdir(parents=True, exist_ok=True)
if not os.path.exists(path):
print(f'real_estate_data.csv не найден. Будет загружен из сети.')
_ = urllib.request.urlretrieve(url, path)
base = pd.read_csv('data/real_estate_data.csv', sep='\t')
# pd.options.mode.chained_assignment = None
data = pd.DataFrame(base)
data.info()
print(data.shape)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 23699 entries, 0 to 23698 Data columns (total 22 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 total_images 23699 non-null int64 1 last_price 23699 non-null float64 2 total_area 23699 non-null float64 3 first_day_exposition 23699 non-null object 4 rooms 23699 non-null int64 5 ceiling_height 14504 non-null float64 6 floors_total 23613 non-null float64 7 living_area 21796 non-null float64 8 floor 23699 non-null int64 9 is_apartment 2775 non-null object 10 studio 23699 non-null bool 11 open_plan 23699 non-null bool 12 kitchen_area 21421 non-null float64 13 balcony 12180 non-null float64 14 locality_name 23650 non-null object 15 airports_nearest 18157 non-null float64 16 cityCenters_nearest 18180 non-null float64 17 parks_around3000 18181 non-null float64 18 parks_nearest 8079 non-null float64 19 ponds_around3000 18181 non-null float64 20 ponds_nearest 9110 non-null float64 21 days_exposition 20518 non-null float64 dtypes: bool(2), float64(14), int64(3), object(3) memory usage: 3.7+ MB (23699, 22)
display(data.head(3))
display(data.describe().T)
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | kitchen_area | balcony | locality_name | airports_nearest | cityCenters_nearest | parks_around3000 | parks_nearest | ponds_around3000 | ponds_nearest | days_exposition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20 | 13000000.0 | 108.0 | 2019-03-07T00:00:00 | 3 | 2.7 | 16.0 | 51.0 | 8 | NaN | ... | 25.0 | NaN | Санкт-Петербург | 18863.0 | 16028.0 | 1.0 | 482.0 | 2.0 | 755.0 | NaN |
| 1 | 7 | 3350000.0 | 40.4 | 2018-12-04T00:00:00 | 1 | NaN | 11.0 | 18.6 | 1 | NaN | ... | 11.0 | 2.0 | посёлок Шушары | 12817.0 | 18603.0 | 0.0 | NaN | 0.0 | NaN | 81.0 |
| 2 | 10 | 5196000.0 | 56.0 | 2015-08-20T00:00:00 | 2 | NaN | 5.0 | 34.3 | 4 | NaN | ... | 8.3 | 0.0 | Санкт-Петербург | 21741.0 | 13933.0 | 1.0 | 90.0 | 2.0 | 574.0 | 558.0 |
3 rows × 22 columns
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| total_images | 23699.0 | 9.858475e+00 | 5.682529e+00 | 0.0 | 6.00 | 9.00 | 14.0 | 50.0 |
| last_price | 23699.0 | 6.541549e+06 | 1.088701e+07 | 12190.0 | 3400000.00 | 4650000.00 | 6800000.0 | 763000000.0 |
| total_area | 23699.0 | 6.034865e+01 | 3.565408e+01 | 12.0 | 40.00 | 52.00 | 69.9 | 900.0 |
| rooms | 23699.0 | 2.070636e+00 | 1.078405e+00 | 0.0 | 1.00 | 2.00 | 3.0 | 19.0 |
| ceiling_height | 14504.0 | 2.771499e+00 | 1.261056e+00 | 1.0 | 2.52 | 2.65 | 2.8 | 100.0 |
| floors_total | 23613.0 | 1.067382e+01 | 6.597173e+00 | 1.0 | 5.00 | 9.00 | 16.0 | 60.0 |
| living_area | 21796.0 | 3.445785e+01 | 2.203045e+01 | 2.0 | 18.60 | 30.00 | 42.3 | 409.7 |
| floor | 23699.0 | 5.892358e+00 | 4.885249e+00 | 1.0 | 2.00 | 4.00 | 8.0 | 33.0 |
| kitchen_area | 21421.0 | 1.056981e+01 | 5.905438e+00 | 1.3 | 7.00 | 9.10 | 12.0 | 112.0 |
| balcony | 12180.0 | 1.150082e+00 | 1.071300e+00 | 0.0 | 0.00 | 1.00 | 2.0 | 5.0 |
| airports_nearest | 18157.0 | 2.879367e+04 | 1.263088e+04 | 0.0 | 18585.00 | 26726.00 | 37273.0 | 84869.0 |
| cityCenters_nearest | 18180.0 | 1.419128e+04 | 8.608386e+03 | 181.0 | 9238.00 | 13098.50 | 16293.0 | 65968.0 |
| parks_around3000 | 18181.0 | 6.114075e-01 | 8.020736e-01 | 0.0 | 0.00 | 0.00 | 1.0 | 3.0 |
| parks_nearest | 8079.0 | 4.908046e+02 | 3.423180e+02 | 1.0 | 288.00 | 455.00 | 612.0 | 3190.0 |
| ponds_around3000 | 18181.0 | 7.702547e-01 | 9.383456e-01 | 0.0 | 0.00 | 1.00 | 1.0 | 3.0 |
| ponds_nearest | 9110.0 | 5.179809e+02 | 2.777206e+02 | 13.0 | 294.00 | 502.00 | 729.0 | 1344.0 |
| days_exposition | 20518.0 | 1.808886e+02 | 2.197280e+02 | 1.0 | 45.00 | 95.00 | 232.0 | 1580.0 |
Есть пропуски в данных, но отрицательных значений нет. В датасете присутствуют значения различных типов, а также выбросы в данных
def get_empty_cols(data):
'''Создание кортежа с столбцами, в которых присутствуют пропущенные значения'''
cols_had_null = {col: round(data[col].isna().sum() / data.shape[0], 3) for col in data.columns}
return {k: v for k, v in cols_had_null.items() if v}
def print_na_info(data):
print("\n".join(["{:<25}{:<10.1%}".format(k, v) for k, v in get_empty_cols(data).items()]))
print_na_info(data)
ceiling_height 38.8% floors_total 0.4% living_area 8.0% is_apartment 88.3% kitchen_area 9.6% balcony 48.6% locality_name 0.2% airports_nearest 23.4% cityCenters_nearest 23.3% parks_around3000 23.3% parks_nearest 65.9% ponds_around3000 23.3% ponds_nearest 61.6% days_exposition 13.4%
data = data[~data['locality_name'].isna()]
def parse_locality(row):
i = 0
while i < len(row):
if row[i].isupper():
break
i += 1
return row[i:].lower().replace('ё', 'e').capitalize()
data.loc[:, 'locality_name'] = data.locality_name.apply(parse_locality)
locality_name_freq = data.locality_name.value_counts().to_frame()
pd.concat([locality_name_freq.head(), locality_name_freq.tail()])
| locality_name | |
|---|---|
| Санкт-петербург | 15721 |
| Мурино | 590 |
| Кудрово | 472 |
| Шушары | 440 |
| Всеволожск | 398 |
| Пельгора | 1 |
| Каложицы | 1 |
| Платформа 69-й километр | 1 |
| Почап | 1 |
| Дзержинского | 1 |
data.is_apartment = data.apply(lambda x: True if x['rooms'] > 1 and x['studio'] is False else False , axis=1)
"{:.1%}".format(data.is_apartment.isna().sum() / data.shape[0] * 100)
'0.0%'
def insert_by_locality(data, col, fill_by_med=True):
medians = data.groupby('locality_name')[col].median().dropna()
for index, value in zip(medians.index, medians):
data.loc[(data[col].isna()) & (data['locality_name'] == index), col] = value
print('{:20} is filled by median of locality data'.format(col), end=' ')
if data[col].isna().sum() and fill_by_med:
data.loc[:, col] = data.fillna(data[col].median())
print('& been filled by median of col')
else:
print()
return data
def apply_insert_by_locality_for_cols(data, cols, fill_by_med=True):
for col in cols:
data = insert_by_locality(data, col, fill_by_med=fill_by_med)
return data
first_part = 'ceiling_height', 'floors_total', 'living_area', 'kitchen_area', 'balcony', 'days_exposition'
second_part = 'airports_nearest', 'cityCenters_nearest', 'parks_around3000', 'parks_nearest',\
'ponds_around3000', 'ponds_nearest'
data = apply_insert_by_locality_for_cols(data, first_part)
data = apply_insert_by_locality_for_cols(data, second_part, fill_by_med=False)
print("#" * 100)
print_na_info(data)
ceiling_height is filled by median of locality data & been filled by median of col floors_total is filled by median of locality data living_area is filled by median of locality data & been filled by median of col kitchen_area is filled by median of locality data & been filled by median of col balcony is filled by median of locality data & been filled by median of col days_exposition is filled by median of locality data & been filled by median of col airports_nearest is filled by median of locality data cityCenters_nearest is filled by median of locality data parks_around3000 is filled by median of locality data parks_nearest is filled by median of locality data ponds_around3000 is filled by median of locality data ponds_nearest is filled by median of locality data #################################################################################################### airports_nearest 20.4% cityCenters_nearest 20.4% parks_around3000 20.4% parks_nearest 25.4% ponds_around3000 20.4% ponds_nearest 20.9%
def parse_two_cols(data, f, s):
'''
Нахождение данных в столбце s для столбца f DataFrame data
Заполнение спец значением -1 и перевод к т д int
'''
def appl(row):
if row[s] > 0 and f != 'cityCenters_nearest':
return 3000
else:
return row[f]
is_check = data[f].isna().sum() == ((data[f].isna()) & (data[s].isna())).sum()
data[s] = data[s].fillna(-1.0).astype(int)
if is_check:
print(f'There\'s no reason to search info in {s} for {f}')
else:
print(f'Search info in {s} for {f}')
data[f] = data.apply(appl, axis=1)
data[f] = data[f].fillna(-1.0).astype(int)
if s != 'airports_nearest':
data = data.drop(s, axis=1)
return data
cols_parse = ('parks_around3000', 'parks_nearest'), ('ponds_around3000', 'ponds_nearest'),\
('airports_nearest', 'cityCenters_nearest')
cols_parse = tuple([item[::-1] for item in cols_parse])
for cols in cols_parse:
data = parse_two_cols(data, *cols)
Search info in parks_around3000 for parks_nearest Search info in ponds_around3000 for ponds_nearest There's no reason to search info in airports_nearest for cityCenters_nearest
cols_parse = 'airports_nearest', 'cityCenters_nearest', 'parks_nearest', 'ponds_nearest'
data.loc[:, cols_parse].describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| airports_nearest | 23650.0 | 23512.449852 | 16667.539974 | -1.0 | 11942.00 | 23140.0 | 35841.0 | 84869.0 |
| cityCenters_nearest | 23650.0 | 11511.521268 | 9633.272673 | -1.0 | 3870.25 | 11753.0 | 15743.0 | 65968.0 |
| parks_nearest | 23650.0 | 1210.685666 | 1300.745383 | -1.0 | -1.00 | 460.0 | 3000.0 | 3190.0 |
| ponds_nearest | 23650.0 | 1425.453362 | 1330.468317 | -1.0 | 503.00 | 503.0 | 3000.0 | 3000.0 |
for col in ['parks_nearest', 'ponds_nearest']:
data[col] = data[col].astype(np.uint16)
Были обработаны отсутствия значений в столбцах locality_name, apartmnent, ceiling_height, floors_total, living_area, kitchen_area, balcony, days_exposition, airports_nearest, cityCenters_nearest, parks_around3000, parks_nearest, ponds_around3000, ponds_nearest. (с помощью группировки по локации удалось заполнить пропуски в данных, но не полностью- оставшиеся пропуски были заполнены медианами по соответствующим столбцам) Также были обработаны значения столбца locality_name (выделены наименования)
data["floors_total"] = data.floors_total.astype(np.uint8)
data["balcony"] = data.balcony.astype(np.uint8)
data['days_exposition'] = data.days_exposition.astype(np.uint32)
data['last_price'] = data.last_price.astype(np.uint32)
data.columns = [
'total_images', 'price', 'total_area', 'first_day_exp',
'rooms', 'ceiling_height', 'floors_total', 'living_area', 'floor',
'apartment', 'studio', 'open_plan', 'kitchen_area', 'balcony',
'location', 'air_nearest', 'city_nearest',
'parks_nearest', 'ponds_nearest', 'days_exp'
]
Были изменены типы данных столбцов с тд float на int floors_total, balcony, days_exposition,last_price.
def describe_enhanced(data, list_cols):
'''
Расширение базового метода `describe` нижниму и верхними оценками границ
выбросов в данных (интерквартильный размах и 3 сигмы)
'''
descr = data[list_cols].describe().T
descr["low_std"] = descr["mean"] - descr["std"] * 3
descr["low_iqr"] = descr["25%"] - (descr["75%"] - descr["25%"]) * 1.5
descr["up_iqr"] = descr["75%"] + (descr["75%"] - descr["25%"]) * 1.5
descr["up_std"] = descr["mean"] + descr["std"] * 3
if isinstance(descr, pd.Series):
return descr.to_frame().T
return descr
def del_anomal_values(data, info_descr, list_cols):
for col in list_cols:
low, up = info_descr.loc[col, 'low_iqr'], info_descr.loc[col, 'up_iqr']
data = data[(low < data[col]) & (data[col] < up)]
return data
В Санкт-Петербурге УН жилой площади также 9 кв. метров на человека в отдельных домах и квартирах, 15 кв. метров — для коммуналок.
col_info = ['kitchen_area', 'living_area', 'total_area'], ['Площадь кухни', 'Жилая площадь', 'Общая площадь']
areas_info = describe_enhanced(data, col_info[0])
display(areas_info)
fig = px.histogram(data, x=col_info[0],
barmode="overlay",
marginal="box",
range_x=(areas_info["min"].min(), areas_info["up_std"].max()),
title="Распределения " + ", ".join(map(lambda l: f"`{l}`", col_info[1])),
)
fig.update_layout(
xaxis_title='Площадь (кв м)',
yaxis_title='Кол-во'
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kitchen_area | 23650.0 | 10.465152 | 5.631919 | 1.3 | 7.2 | 9.6 | 11.4575 | 112.0 | -6.430605 | 0.81375 | 17.84375 | 27.360910 |
| living_area | 23650.0 | 34.054007 | 21.226308 | 2.0 | 19.0 | 30.4 | 41.1000 | 409.7 | -29.624918 | -14.15000 | 74.25000 | 97.732932 |
| total_area | 23650.0 | 60.329069 | 35.661808 | 12.0 | 40.0 | 52.0 | 69.7000 | 900.0 | -46.656355 | -4.55000 | 114.25000 | 167.314493 |
data = del_anomal_values(data, areas_info, col_info[0])
Были проанализированы площадь кухни, жилая и общая площади. Было выявлено, что имеют место аномалии (выбросы)
в данных. С помощью границы (Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных
data.loc[:, 'price_m'] = data.price.apply(lambda x: x / 10 ** 6).astype(np.uint8)
col_info = 'price_m', 'Цена на момент снятия объявления, млн руб'
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
barmode="overlay",
marginal="box",
title=col_info[1],
)
fig.update_layout(
xaxis_title=col_info[1],
yaxis_title="Кол-во"
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| price_m | 21409.0 | 4.556402 | 2.953964 | 0.0 | 3.0 | 4.0 | 6.0 | 53.0 | -4.305491 | -1.5 | 10.5 | 13.418294 |
data = del_anomal_values(data, info, [col_info[0]])
Был добавлен столбец со значениями цены недвижимости в млн руб. Проанализировали цены на момент снятия объявления.С помощью границы (Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных
col_info = 'rooms', 'Количество комнат'
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
barmode="overlay",
marginal="box",
title=col_info[1],
nbins=30
)
fig.update_layout(
xaxis_title=col_info[1],
yaxis_title="Кол-во"
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| rooms | 20561.0 | 1.893536 | 0.882271 | 0.0 | 1.0 | 2.0 | 3.0 | 6.0 | -0.753277 | -2.0 | 6.0 | 4.54035 |
Был проанализирован параметр - количество комнат, получилось выявить, что аномалии (выбросы) отсутствуют
col_info = 'ceiling_height', 'Высота потолков (м)'
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
barmode="overlay",
marginal="box",
title=col_info[1],
)
fig.update_layout(
xaxis_title=col_info[1],
yaxis_title="Кол-во"
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ceiling_height | 20561.0 | 2.699104 | 0.819571 | 1.0 | 2.55 | 2.7 | 2.7 | 32.0 | 0.24039 | 2.325 | 2.925 | 5.157818 |
data = del_anomal_values(data, info, [col_info[0]])
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
barmode="overlay",
marginal="box",
title=col_info[1],
nbins=50
)
fig.update_layout(
xaxis_title=col_info[1],
yaxis_title="Кол-во"
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ceiling_height | 19024.0 | 2.633329 | 0.09716 | 2.34 | 2.55 | 2.65 | 2.7 | 2.92 | 2.34185 | 2.325 | 2.925 | 2.924808 |
Была проанализирована высота потолков (м), получилось выявить, что имеют место аномалии (выбросы)
в данных. С помощью границы (Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных
data.loc[:, 'price_sq_m'] = (data['price'] / data['total_area'])
data.loc[:, 'date_exp'] = pd.to_datetime(data['first_day_exp'])
data.loc[:, 'day_exp'] = data['date_exp'].dt.day
data.loc[:, 'month_exp'] = data['date_exp'].dt.month
data.loc[:, 'year_exp'] = data['date_exp'].dt.year
data = data.drop('first_day_exp', axis=1)
def apl_floor(row):
if row.floor == row.floors_total:
return 'Последний'
if row.floor == 1:
return 'Первый'
return 'Другой'
data.loc[:, 'floor'] = data.apply(apl_floor, axis=1)
data = data.drop('floors_total', axis=1)
data.loc[:, 'living_total'] = round(data.living_area / data.total_area, 3)
data.loc[:, 'kitchen_total'] = round(data.kitchen_area / data.total_area, 3)
data.loc[:, 'city_nearest_km'] = (data.city_nearest // 1000).astype(np.uint8)
Были добавлены/изменены след данные: price_sq_m, date_exp, day_exp, month_exp, year_exp,
floor, living_total, kitchen_total, city_nearest_km
balcony — число балконовceiling_height — высота потолков (м)city_nearest — расстояние до центра города (м)days_exp — сколько дней было размещено объявление (от публикации до снятия)apartment — апартаменты (булев тип)kitchen_area — площадь кухни в квадратных метрах (м²)price — цена на момент снятия с публикации (руб)living_area — жилая площадь в квадратных метрах(м²)localation — название населённого пунктаopen_plan — свободная планировка (булев тип)parks_nearest — расстояние до ближайшего парка (м)ponds_nearest — расстояние до ближайшего водоёма (м)rooms — число комнатstudio — квартира-студия (булев тип)total_area — площадь квартиры в квадратных метрах (м²)total_images — число фотографий квартиры в объявленииprice_sq_m цена квадратного метраdate_exp дата создания объявленияday_exp день создания объявленияmonth_exp месяц создания объявленияyear_exp год создания объявленияfloor этаж (теперь признак категориальный)living_total отношение жилой к общей площадиkitchen_total отношение площади кухни к общейprice_m — цена на момент снятия с публикации (млн. руб)city_nearest_km — расстояние до центра города (м)for col in ['total_images', 'rooms', 'ceiling_height', 'balcony', 'days_exp',\
'day_exp', 'month_exp', 'year_exp', 'city_nearest_km']:
data.loc[:, col] = data.loc[:, col].astype(np.uint16)
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/2396148798.py:3: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
col_info = ['kitchen_total', 'living_total'], ['Пл-дь кухни к общей', 'Жилая пл-дь к общей']
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data, x=col_info[0],
barmode="overlay",
marginal="box",
range_x=(info["min"].min(), info["up_std"].max()),
title="Распределения " + ", ".join(map(lambda l: f"`{l}`", col_info[1])),
)
fig.update_layout(
xaxis_title='Значения отношений соответствующих площадей',
yaxis_title='Кол-во'
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kitchen_total | 19024.0 | 0.191825 | 0.070288 | 0.044 | 0.138 | 0.179 | 0.237 | 0.825 | -0.019040 | -0.0105 | 0.3855 | 0.402691 |
| living_total | 19024.0 | 0.570148 | 0.118752 | 0.070 | 0.495 | 0.567 | 0.640 | 2.408 | 0.213893 | 0.2775 | 0.8575 | 0.926404 |
data = del_anomal_values(data, info, col_info[0])
Были проанализированы данные столбцов отношений площадей кухни к общей и жилой к общей площади соотв-но.
Было выявлено, что имеют место аномалии (выбросы) в данных. С помощью границы
(Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных
col_info = "days_exp", "Время продажи квартиры"
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
barmode="overlay",
marginal="box",
title=col_info[1],
nbins=50
)
fig.update_layout(
xaxis_title=col_info[1],
yaxis_title="Кол-во"
)
| count | mean | std | min | 25% | 50% | 75% | max | low_std | low_iqr | up_iqr | up_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| days_exp | 18414.0 | 161.101173 | 196.08347 | 1.0 | 45.0 | 95.0 | 190.0 | 1580.0 | -427.149236 | -172.5 | 407.5 | 749.351582 |
data = del_anomal_values(data, info, [col_info[0]])
Были проанализированы данные столбца days_exp - количество дней доступа к объявлению о недвижимости.
Было выявлено, что имеют место аномалии (выбросы) в данных. С помощью границы
(Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных
col_parse = 'price_sq_m'
cols_parse = ['total_area', 'rooms', 'floor', 'city_nearest_km']
fig, axises = plt.subplots(1, len(cols_parse))
for col, axis in zip(cols_parse, axises):
if col != 'floor':
correlation = round(data[col_parse].corr(data[col]), 2)
print('{} ~ {: ^13} ->{: ^10}'.format(col_parse, col, correlation))
if col == 'floor':
(data
.groupby(col)[col_parse].median().reset_index()
.plot(kind='bar', x='floor', y=col_parse, label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
)
elif col == 'rooms':
(data
.groupby(col)[col_parse].median().reset_index(drop=True)
.plot(label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
)
else:
(data
.groupby(col)[col_parse].median().reset_index()
.plot(kind='scatter', x=col, y=col_parse, label=col,\
legend=True, grid=True, alpha=0.5, figsize=(17, 7), ax=axis)
)
plt.xlabel(col)
plt.show()
price_sq_m ~ total_area -> -0.12 price_sq_m ~ rooms -> -0.26 price_sq_m ~ city_nearest_km -> -0.65
data_corr = data.corr()
data_corr_trunc = pd.DataFrame(np.tril(data_corr, k=-1),
columns=data_corr.columns,
index=data_corr.index
)
px.imshow(data_corr_trunc.replace({0: None}),
text_auto=".1f",
zmin=-1,
zmax=1,
width=700,
height=700
)
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/1713752210.py:1: FutureWarning: The default value of numeric_only in DataFrame.corr is deprecated. In a future version, it will default to False. Select only valid columns or specify the value of numeric_only to silence this warning.
col_parse = 'price_sq_m'
cols_parse = ['day_exp', 'month_exp', 'year_exp']
data_corr = data[cols_parse + [col_parse]].corr()
data_corr_trunc = pd.DataFrame(np.tril(data_corr, k=-1),
index=data_corr.index,
columns=data_corr.columns)
fig = px.imshow(data_corr_trunc.replace({0: None}),
zmin=-1, zmax=1, text_auto=".1f")
fig.show()
px.line(data.groupby(col)[cols_parse].median(), log_y=True).show()
fig = px.line(data.groupby('city_nearest_km').agg(
price_sq_m_median=("price_sq_m", np.median),
price_sq_m_mean=("price_sq_m", np.mean),
))
fig.update_layout(
title="Title",
xaxis_title="x",
yaxis_title="y",
)
Больше всего на стоимость влияют след параметры: удалённость от города (км) и количество комнат
До ~70км зависимость не является линейной - это можно связать с множеством районов в пределах города, которые отличаются
по уровню и качеству застройки, после граничного значения км жилья +- одинаковое и видная прямая завимисимость между
удалённостью и ценой квадратного метра (обратная зависимость)
Меньшую корреляцию с ценой за кв метр имеет этаж (на первом этаже меньшее значение цены площади кв метра)
Наибольшую цену кв метра, имеют объявления, созданные в 27-28 числах августа. Также наблюдается тенденция в росте
цены кв метра с 2016 по 2019 год.
data_top_10_count = (data
.groupby('location').agg({'rooms':'count', 'price_sq_m':'mean'}).reset_index()
.sort_values(by='rooms', ascending=False).reset_index(drop=True)
)
data_top_10_count.loc[:, 'price_sq_m'] = data_top_10_count['price_sq_m'].astype(int)
data_top_10_count.columns = ['location', 'count', 'price_sq_m']
print('{:#^70}'.format('Первые 10 нас. пунктов по количеству объявлений'))
display(data_top_10_count.head(10))
###########Первые 10 нас. пунктов по количеству объявлений############
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/1076292836.py:5: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
| location | count | price_sq_m | |
|---|---|---|---|
| 0 | Санкт-петербург | 10303 | 103944 |
| 1 | Мурино | 458 | 85616 |
| 2 | Шушары | 378 | 77880 |
| 3 | Кудрово | 343 | 95485 |
| 4 | Всеволожск | 319 | 66775 |
| 5 | Колпино | 285 | 75232 |
| 6 | Парголово | 267 | 90432 |
| 7 | Пушкин | 260 | 99324 |
| 8 | Гатчина | 244 | 68386 |
| 9 | Выборг | 174 | 57208 |
data_top_price = (data
# .groupby('location')['price_sq_m'].mean().reset_index()
.groupby('location').agg({'rooms':'count', 'price_sq_m':'mean'}).reset_index()
.sort_values(by='price_sq_m', ascending=False)
)
data_top_price.loc[:, 'price_sq_m'] = data_top_price['price_sq_m'].astype(int)
data_top_price.columns = ['location', 'count', 'price_sq_m']
display('{:#^70}'.format('Топ по цене (первые 10)'))
display(data_top_price.head(10).reset_index(drop=True))
display('{:#^70}'.format('Топ по цене (последние 10)'))
display(data_top_price.tail(10).reset_index(drop=True))
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/965368589.py:6: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
'#######################Топ по цене (первые 10)########################'
| location | count | price_sq_m | |
|---|---|---|---|
| 0 | Лисий нос | 2 | 113728 |
| 1 | Санкт-петербург | 10303 | 103944 |
| 2 | Сестрорецк | 113 | 102439 |
| 3 | Зеленогорск | 20 | 100123 |
| 4 | Пушкин | 260 | 99324 |
| 5 | Мистолово | 8 | 97145 |
| 6 | Левашово | 1 | 96997 |
| 7 | Кудрово | 343 | 95485 |
| 8 | Парголово | 267 | 90432 |
| 9 | Стрельна | 35 | 88627 |
'######################Топ по цене (последние 10)######################'
| location | count | price_sq_m | |
|---|---|---|---|
| 0 | Житково | 1 | 14264 |
| 1 | Ефимовский | 2 | 14149 |
| 2 | Ям-тесово | 2 | 13711 |
| 3 | Сижно | 1 | 13709 |
| 4 | Тeсово-4 | 1 | 12931 |
| 5 | Совхозный | 2 | 12629 |
| 6 | Выскатка | 2 | 12335 |
| 7 | Вахнова кара | 1 | 11688 |
| 8 | Свирь | 2 | 11481 |
| 9 | Старополье | 3 | 11206 |
Были найдены первые 10 нас пунктов по кол-ву объявлений (Петербург, Мурино, Шушары).
Также были обнаружены первые и последние 10 нас пунктов по цене квадратного метра (см 2 таблицы выше).
city_nearest_info = describe_enhanced(data, ['city_nearest_km'])
city_nearest_info.T
| city_nearest_km | |
|---|---|
| count | 16595.000000 |
| mean | 71.118530 |
| std | 101.371557 |
| min | 0.000000 |
| 25% | 12.000000 |
| 50% | 15.000000 |
| 75% | 35.000000 |
| max | 255.000000 |
| low_std | -232.996142 |
| low_iqr | -22.500000 |
| up_iqr | 69.500000 |
| up_std | 375.233201 |
ax = (data
.query('location == "Санкт-Петербург"')
.groupby('city_nearest_km')['price_sq_m'].mean()
.plot(label='Цена кв метра в Петербурге', figsize=(15, 7), grid=True, legend=True)
)
plt.xlabel('Удалённость от центра Петербурга')
plt.ylabel('Цена кв метра недвижимости')
plt.show()
border_center = 7
Посчитали среднюю цену для каждого километра и постройте график. Чем больше удаляемся от центра, тем цена ниже
(За исключением пары мест на 42 и 59 км - может быть связано с элитными пригородскими коттеджными посёлками,
в которых цены за кв метр существенно выше). Определили границу центральной зоны Петербурга - по графику смогли определить
значение (примерно 7-ой км является границей этой зоны, имеющей форму приблизительно окружности)
# cc - city_center
list_cols = 'location', 'total_area', 'price_m', 'rooms', 'ceiling_height', 'price_sq_m', 'city_nearest_km', 'floor',\
'day_exp', 'month_exp', 'year_exp'
data_cc = data.query('0 <= city_nearest_km <= @border_center').loc[:, list_cols]
data_cc.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| total_area | 1104.0 | 58.546685 | 18.641605 | 17.0 | 43.975000 | 56.900000 | 71.00000 | 114.200000 |
| price_m | 1104.0 | 6.206522 | 1.928672 | 1.0 | 5.000000 | 6.000000 | 8.00000 | 10.000000 |
| rooms | 1104.0 | 2.092391 | 0.889434 | 0.0 | 1.000000 | 2.000000 | 3.00000 | 5.000000 |
| ceiling_height | 1104.0 | 2.000000 | 0.000000 | 2.0 | 2.000000 | 2.000000 | 2.00000 | 2.000000 |
| price_sq_m | 1104.0 | 118871.933953 | 28670.807152 | 26250.0 | 98585.239033 | 114557.844492 | 136043.48238 | 262711.864407 |
| city_nearest_km | 1104.0 | 4.681159 | 1.696823 | 0.0 | 4.000000 | 5.000000 | 6.00000 | 7.000000 |
| day_exp | 1104.0 | 15.083333 | 8.600446 | 1.0 | 8.000000 | 15.000000 | 23.00000 | 31.000000 |
| month_exp | 1104.0 | 6.664855 | 3.399109 | 1.0 | 4.000000 | 7.000000 | 10.00000 | 12.000000 |
| year_exp | 1104.0 | 2017.352355 | 0.892161 | 2015.0 | 2017.000000 | 2017.000000 | 2018.00000 | 2019.000000 |
col_parse = 'price_sq_m'
cols_parse = ['total_area', 'rooms', 'floor', 'city_nearest_km']
fig, axises = plt.subplots(1, len(cols_parse))
for col, axis in zip(cols_parse, axises):
if col != 'floor':
correlation = round(data_cc[col_parse].corr(data_cc[col]), 2)
print('{} ~ {: ^13} ->{: ^10}'.format(col_parse, col, correlation))
if col == 'floor':
(data_cc
.groupby(col)[col_parse].median().reset_index()
.plot(kind='bar', x='floor', y=col_parse, label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
)
elif col == 'rooms':
(data_cc
.groupby(col)[col_parse].median().reset_index(drop=True)
.plot(label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
)
else:
(data_cc
.groupby(col)[col_parse].median().reset_index()
.plot(kind='scatter', x=col, y=col_parse, label=col,\
legend=True, grid=True, alpha=0.7, figsize=(17, 7), ax=axis)
)
plt.xlabel(col)
plt.show()
price_sq_m ~ total_area -> -0.46 price_sq_m ~ rooms -> -0.52 price_sq_m ~ city_nearest_km -> 0.04
col_parse = 'price_sq_m'
cols_parse = ['day_exp', 'month_exp', 'year_exp']
fig, axises = plt.subplots(1, len(cols_parse))
for col, axis in zip(cols_parse, axises):
correlation = round(data_cc[col_parse].corr(data_cc[col]), 2)
print('{} ~ {: ^13} ->{: ^10}'.format(col_parse, col, correlation))
(data_cc
.groupby(col)[col_parse].median()
.plot(label='Цена кв метра', legend=True, grid=True, figsize=(17, 7), ax=axis)
)
plt.show()
price_sq_m ~ day_exp -> 0.03 price_sq_m ~ month_exp -> -0.05 price_sq_m ~ year_exp -> 0.18
Больше всего на стоимость квадратного метра в центре города влияют след параметры:
количество комнат (превалирует вариант жилья с одной комнатой) и площадь (в отличие от всей области в целом)
Также отличаются от ситуации по области и зависимость цены от времени продажи - 30-ые числа для старта объявления
оказываются наиболее удачными в плане продажи жилья и первой четверти года (март месяц). Что же касаемо тенденции
в росте цены кв метра, то можно утверждать, что цены на кв метр в центре не так подвластны снижению.
Рост наблюдается с 2015 по 2019 год. (нет "обвала" цен в 15 году, как наблюдается для всеё области в целом)